POST /v2/products

[!info] 概述 创建新的 ANY_DAY_TICKET(任意日门票)产品。在购买时才绑定到具体活动,而非创建时。

[!tip] 核心特性

  • 自动 Event Catalog:自动使用用户的活动目录
  • 无 Event 绑定Product.eventId 始终为 null
  • 灵活 Event 选择:购买时通过 variantInfo.eventId 动态绑定
  • 手续费计算:自动计算交易手续费、平台费用、税费

请求信息

请求地址

POST /v2/products

请求头 (Headers)

Header 类型 必填 说明
authorization string Bearer Token (JWT)
content-type string application/json
timezone string 用户时区 (如 Asia/Shanghai)
from string 客户端标识 (如 client)

请求体 (Body)

请求参数结构

interface CreateProductV2Dto {
  // ========== 必填字段 ==========
  title: string;                              // 产品标题
  status: ProductStatus;                       // 产品状态
  listingType: ProductType.ANY_DAY_TICKET;    // 产品类型(必须为 ANY_DAY_TICKET)
  deliveryMethod: ProductDeliveryMethod;      // 交付方式(QR_CODE / IN_PERSON / THIRD_PARTY_ISSUED)
  images: CreateProductImage[];               // 产品图片数组

  // ========== 可选字段 ==========
  // 商家/目录
  merchantId?: string;                         // 商家 ID(V2 可选,自动解析为 Event Catalog)

  // 内容
  bodyHtml?: string;                           // HTML 正文
  bodyJson?: string;                           // Lexical JSON 正文

  // 选项与变体
  options?: CreateProductOption[];              // 规格选项(默认自动生成)
  variants: CreateProductVariant[];             // 变体数组(至少 1 个)

  // 配送设置
  shippingType?: ShippingType;                 // 运输类型(默认 NO_SHIPPING_REQUIRED)
  deliveryTime?: [number, number];             // 配送时间范围(天)
  additionalShippingFee?: number;               // 额外运费
  autoFulfill?: boolean;                        // 自动履约(默认 false)
  shippingOptions?: ProductShippingOptionRequest[];  // 运费选项
  shippingNote?: string;                       // 配送说明

  // 第三方配送消息 (KAT-9452)
  thirdPartyDeliveryMessage?: string;           // 第三方配送消息(最大 500 字符)

  // 状态与标记
  followExternalStatus?: boolean;               // 是否跟随外部状态(默认 true)
  isUsed?: ProductUseStatus;                    // 产品使用状态(NWT / USED)
  isFeatured?: boolean;                         // 是否精选
  featuredScore?: number;                       // 精选评分

  // 税收配置
  taxEnable?: boolean;                          // 是否启用税收
  taxJarCategory?: string;                      // TaxJar 分类
  overrideEventTax?: boolean;                   // 是否覆盖 Event 税收 (KAT-10224)
  customTaxRate?: number | null;                // 自定义税率(0-100,null 表示禁用)

  // 佣金
  commissionRate?: number;                      // 佣金比例
  catalogCommissionRate?: number;                // 目录佣金比例

  // 链接
  links?: object;                               // 产品链接

  // ANY_DAY_TICKET 特有字段
  // eventId 始终为 null(购买时才绑定)
  eventId?: string;                             // ❌ 不支持(ANY_DAY_TICKET 无需提供)

  // 门票筛选配置 (KAT-10412)
  atDoorTicketConfig?: AtDoorTicketConfig;

  // 生命周期状态
  lifecycleStatus?: ProductLifecycleStatus;    // 生命周期状态(默认 NORMAL)
  stopSellingAfterDisplay?: string;            // 停止销售时间显示

  // 产品表单 (KAT-8634)
  isProductFormEnabled?: boolean;              // 是否启用产品表单
  productForm?: CreateUserContactFormRequest; // 产品表单配置

  // 同步设置
  excludeFromPostSync?: boolean;                // 排除自动同步(默认 false)

  // 终端销售
  allowAtDoorSales?: boolean;                   // 允许终端销售(默认 false)
}

CreateProductImage 结构

interface CreateProductImage {
  id: string;                    // 图片 ID(Cloudinary public_id)
  src: string;                   // 图片 URL
  width: number;                  // 图片宽度
  height: number;                 // 图片高度
  mediaType: MediaType;          // 媒体类型(IMAGE / VIDEO)
  source: ProductImageSource;     // 图片来源
  position: number;               // 图片位置
  setThumbnail?: boolean;         // 是否设为缩略图
  origin?: {                      // 来源信息
    id: string;
    width: number;
    height: number;
    position: number;
  };
  productImageType?: ProductImageType;  // 图片类型(PRODUCT / VARIANT)
}

CreateProductOption 结构

interface CreateProductOption {
  name: string;                   // 选项名称(如 "Title", "Size")
  values: string[];               // 选项值数组(如 ["Default Title"])
  images?: any[];                 // 选项图片
}

CreateProductVariant 结构

interface CreateProductVariant {
  // ========== 必填字段 ==========
  price: string | number;          // 价格(字符串或数字)
  inventoryQuantity: number;       // 库存数量
  option?: {                       // 规格选项(自动生成 title)
    option1?: string;
    option2?: string;
    option3?: string;
  };

  // ========== ANY_DAY_TICKET 专属字段 ==========
  ticketPrice?: number;            // 票面价格(用于手续费计算)
  fees?: number;                   // 手续费(自动计算)
  transactionFee?: {               // 交易手续费明细
    platformFee: number;          // 平台费用
    customFee: number;            // 自定义费用
    customFeeBreakdown?: {        // 费用明细
      TAX: {
        unitFixedFee: number;     // 固定税费
        unitPercentageFee: number; // 百分比税费
        itemFee?: number;         // 项目费用
      }
    };
    transactionItemFee: number;    // 总手续费
  };

  // ========== 可选字段 ==========
  priceAnchor?: string | number;    // 锚定价格(用于划线显示)
  compareAtPrice?: string | number;  // 比较价格

  // 购买数量限制 (KAT-9346)
  isMinPurchaseQuantityEnabled?: boolean;
  minPurchaseQuantity?: number;
  isMaxPurchaseQuantityEnabled?: boolean;
  maxPurchaseQuantity?: number;
  isPackSizeEnabled?: boolean;
  packSize?: number;

  // SKU
  sku?: string;

  // 图片
  imageId?: string;
}

AtDoorTicketConfig 结构 (KAT-10412)

interface AtDoorTicketConfig {
  includeTitleText?: string | null;       // 包含指定标题的活动才显示
  excludeTitleText?: string | null;       // 排除指定标题的活动
  eventSelectionWindowHours?: number | null;  // 事件选择时间窗口(小时)
}

CreateUserContactFormRequest 结构 (KAT-8634)

interface CreateUserContactFormRequest {
  title: string;                           // 表单标题
  subtitle?: string;                        // 表单副标题
  showImage?: boolean;                      // 是否显示图片
  contactFormFields: ContactFormField[];   // 表单字段数组
}

interface ContactFormField {
  title: string;                            // 字段标题
  field: UserContactFormEnum;               // 字段类型
  required: boolean;                         // 是否必填
  description?: string;                      // 字段描述
  tooltip?: string;                          // 字段提示
  extensions?: object;                        // 字段扩展配置
}

响应结构

响应格式

ProductResponse

响应字段说明

见 [[GET /v2/products/any-day-tickets]] 响应字段说明


成功示例

请求示例 (cURL)

curl 'https://release.katana-api.1m.app/v2/products' \
  -H 'accept: application/json, text/plain, */*' \
  -H 'authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...' \
  -H 'content-type: application/json' \
  -H 'timezone: Asia/Shanghai' \
  --data-raw '{
    "merchantId": "bf1d0f03-03c7-4c6d-b886-80eea723eab3",
    "shippingType": "NO_SHIPPING_REQUIRED",
    "isFeatured": false,
    "commissionRate": 0,
    "priceSyncImported": false,
    "additionalShippingFee": 0,
    "returnPolicyApplied": false,
    "images": [{
      "id": "80f43627-4bef-49b0-934b-cd63d49f8787",
      "height": 1000,
      "width": 1000,
      "src": "https://res.cloudinary.com/dr9io1zjv/v1755656006/uploaded_images/pt4zctae8zwv8jrljrf7.png",
      "mediaType": "IMAGE",
      "source": "PRE_DESIGNED",
      "position": 1,
      "setThumbnail": true
    }],
    "title": "At-Door General Admission",
    "bodyHtml": "<p>Valid for any upcoming event</p>",
    "status": "ACTIVE",
    "isUsed": "NWT",
    "taxJarCategory": "",
    "platform": "PEAR",
    "autoFulfill": true,
    "options": [{
      "name": "Title",
      "values": ["Default Title"],
      "images": []
    }],
    "variants": [{
      "inventoryQuantity": 100,
      "price": 50,
      "priceAnchor": 0,
      "option": { "option1": "Default Title" },
      "ticketPrice": 50,
      "fees": 5.50,
      "transactionFee": {
        "platformFee": 1.00,
        "customFee": 0,
        "customFeeBreakdown": {
          "TAX": {
            "unitFixedFee": 0,
            "unitPercentageFee": 0.1,
            "itemFee": 0
          }
        },
        "transactionItemFee": 5.50
      }
    }],
    "listingType": "ANY_DAY_TICKET",
    "catalogCommissionRate": 0,
    "deliveryMethod": "QR_CODE",
    "taxEnable": false,
    "customTaxRate": 0,
    "overrideEventTax": true,
    "atDoorTicketConfig": {
      "eventSelectionWindowHours": 24,
      "includeTitleText": "",
      "excludeTitleText": ""
    },
    "thirdPartyDeliveryMessage": "",
    "bodyJson": "{\"root\":{\"children\":[{\"children\":[{\"detail\":0,\"format\":0,\"mode\":\"normal\",\"style\":\"\",\"text\":\"Valid for any upcoming event\",\"type\":\"extended-text\",\"version\":1}],\"direction\":\"ltr\",\"format\":\"\",\"indent\":0,\"type\":\"katana-paragraph\",\"version\":1}]}}"
  }'

响应示例

{
  "id": "018eaabc-1234-5678-9012-fedcba987654",
  "title": "At-Door General Admission",
  "listingType": "ANY_DAY_TICKET",
  "status": "ACTIVE",
  "externalStatus": "ACTIVE",
  "followExternalStatus": true,
  "platform": "PEAR",
  "catalog": {
    "id": "bf1d0f03-03c7-4c6d-b886-80eea723eab3",
    "name": "Event Catalog",
    "catalogType": "EVENT"
  },
  "eventId": null,
  "event": undefined,
  "bodyHtml": "<p>Valid for any upcoming event</p>",
  "bodyText": "Valid for any upcoming event",
  "coverImage": {
    "src": "https://res.cloudinary.com/dr9io1zjv/v1755656006/uploaded_images/pt4zctae8zwv8jrljrf7.png",
    "mediaType": "IMAGE"
  },
  "variants": [
    {
      "id": "variant-uuid-1",
      "position": 1,
      "price": "50.00",
      "inventoryQuantity": 100,
      "title": "Default Title",
      "option": { "option1": "Default Title" },
      "fees": 5.50,
      "transactionFee": {
        "transactionItemFee": 5.50,
        "platformFee": 1.00,
        "customFee": 0,
        "customFeeBreakdown": { "TAX": { "unitFixedFee": 0, "unitPercentageFee": 0.1 } }
      }
    }
  ],
  "priceMin": 50.00,
  "priceMax": 50.00,
  "deliveryMethod": "QR_CODE",
  "shippingType": "NO_SHIPPING_REQUIRED",
  "autoFulfill": true,
  "isAvailable": true,
  "taxEnable": false,
  "isFeatured": false,
  "isUsed": "NWT",
  "createdAt": "2026-03-02T01:00:00Z",
  "updatedAt": "2026-03-02T01:00:00Z",
  "isMultipleDaysPassEnabled": false,
  "atDoorTicketConfig": {
    "eventSelectionWindowHours": 24,
    "includeTitleText": "",
    "excludeTitleText": ""
  },
  "isProductFormEnabled": false,
  "productForm": null
}

错误示例

400 Bad Request - 缺少必填字段

{
  "statusCode": 400,
  "message": ["title should not be empty", "variants must not be empty"],
  "error": "Bad Request"
}

原因:缺少 titlevariants 必填字段。

400 Bad Request - 不支持多日通票

{
  "statusCode": 400,
  "message": "ANY_DAY_TICKET does not support multi-day pass. Use TICKET type for multi-day passes.",
  "error": "Bad Request"
}

原因:ANY_DAY_TICKET 不支持 isMultipleDaysPassEnabled

400 Bad Request - 提供了 eventId

{
  "statusCode": 400,
  "message": "ANY_DAY_TICKET does not require eventId at creation time. Event binding happens at purchase time.",
  "error": "Bad Request"
}

原因:ANY_DAY_TICKET 创建时不应该提供 eventId(在购买时才绑定)。

403 Forbidden - 权限不足

{
  "statusCode": 403,
  "message": "No permission",
  "error": "Forbidden"
}

原因:用户不是 BUSINESS_PARTNER 角色。

400 Bad Request - 手续费计算不匹配

{
  "statusCode": 400,
  "message": "Variant fees or price calculation does not match the expected value",
  "error": "Bad Request"
}

原因:前端传递的 fees 与后端计算的手续费不一致。


业务逻辑

核心流程

┌─────────────────────────────────────────────────────────────────┐
│  1. Controller 层验证                                           │
│     ✅ 用户权限检查 (BUSINESS_PARTNER)                           │
│     ✅ 交付方式验证 (deliveryMethod + shippingType)                 │
│     ✅ 运费配置验证 (shippingOptions)                            │
└─────────────────────────────────────────────────────────────────┘
                            ↓
┌─────────────────────────────────────────────────────────────────┐
│  2. 自动解析商家 ID                                               │
│     if ANY_DAY_TICKET → merchantId = Event Catalog ID            │
│     eventCatalogService.ensureExists(userId)                    │
└─────────────────────────────────────────────────────────────────┘
                            ↓
┌─────────────────────────────────────────────────────────────────┐
│  3. Strategy 层验证 (AnyDayTicketProductStrategy)              │
│     ✅ 标题必填                                                    │
│     ✅ 至少一个变体                                                 │
│     ✅ 目录类型必须是 EVENT                                     │
│     ✅ eventId 必须为空 (或不提供)                              │
│     ✅ 不支持多日通票                                             │
│     ✅ 交付方式必须是 QR_CODE/IN_PERSON/THIRD_PARTY_ISSUED       │
│     ✅ 精选数量限制 (max 50)                                     │
│     ✅ atDoorTicketConfig 验证 (KAT-10412)                      │
│     ✅ 产品表单验证 (KAT-8634)                                   │
│     ✅ 手续费验证 (checkTicketVariantPrices)                       │
└─────────────────────────────────────────────────────────────────┘
                            ↓
┌─────────────────────────────────────────────────────────────────┐
│  4. 数据增强 (enrichForCreate)                                   │
│     - 设置 listingType = ANY_DAY_TICKET                          │
│     - eventId = undefined (始终为空)                              │
│     - deliveryMethod = QR_CODE (默认)                             │
│     - shippingType = NO_SHIPPING_REQUIRED                         │
│     - autoFulfill = true                                          │
│     - taxEnable, overrideEventTax, customTaxRate                   │
│     - atDoorTicketConfig 默认值                                   │
│     - 产品表单字段                                                 │
└─────────────────────────────────────────────────────────────────┘
                            ↓
┌─────────────────────────────────────────────────────────────────┐
│  5. 变体处理 (processVariantsForCreate)                         │
│     - 计算 ticketPrice (默认等于 price)                          │
│     - 计算手续费 (transactionFee, platformFee, customFee)           │
│     - 税率优先级: overrideEventTax → User.globalEventTaxRate → 0%    │
│     - 设置变体默认值 (inventoryManagement, inventoryPolicy 等)     │
│     - 验证手续费与前端传入值一致                                   │
└─────────────────────────────────────────────────────────────────┘
                            ↓
┌─────────────────────────────────────────────────────────────────┐
│  6. 数据库事务 (Phase 1)                                         │
│     prisma.$transaction(async (tx) => {                           │
│       // 创建 Product                                            │
│       const product = await tx.product.create({ ... });             │
│       // 创建 PromoterProduct                                    │
│       await tx.promoterProduct.create({ ... });                    │
│     })                                                             │
└─────────────────────────────────────────────────────────────────┘
                            ↓
┌─────────────────────────────────────────────────────────────────┐
│  7. Phase 2: 添加到 StoreFront 模块 (如果 moduleIds 提供)        │
│     storeFrontModuleItemService.addItemToModulesAtBeginning()     │
└─────────────────────────────────────────────────────────────────┘
                            ↓
┌─────────────────────────────────────────────────────────────────┐
│  8. Phase 3: 处理产品表单 (KAT-8634)                                 │
│     productFormService.handleProductForm(isEnabled, form)         │
└─────────────────────────────────────────────────────────────────┘
                            ↓
┌─────────────────────────────────────────────────────────────────┐
│  9. Phase 4: 后置钩子 (afterCreate)                                │
│     - 搜索引擎索引 (Bull Queue 异步)                             │
│     - 事件发布 (emit EventV2Created)                             │
│     - QR 码模板初始化                                           │
└─────────────────────────────────────────────────────────────────┘
                            ↓
┌─────────────────────────────────────────────────────────────────┐
│  10. 返回完整产品信息                                              │
│     productService.getProduct(product.id, userId)                  │
└─────────────────────────────────────────────────────────────────┘

关键设计决策

1. 自动 Event Catalog

  • ANY_DAY_TICKET 必须在 Event Catalog 中创建
  • merchantId 可选(V2 特性)
  • 系统自动调用 eventCatalogService.ensureExists(userId) 获取/创建 Event Catalog

2. 无 Event 绑定

  • Product.eventId 始终为 null
  • Event 绑定通过 OrderLineItem.variantInfo.eventId购买时存储
  • 前端需调用 GET /product-event/v2/upcoming 获取可选活动列表

3. 税率计算

税率优先级:
1. overrideEventTax = true → 使用 customTaxRate (null = 0%)
2. overrideEventTax = false → 使用 User.globalEventTaxRate
3. 默认 → 0%

手续费计算:
- transactionItemFee = platformFee + customFee
- platformFee = 1.00 (固定)
- customFee = TAX 税费

4. 不支持多日通票

  • 请求中包含 isMultipleDaysPassEnabled = true 会抛出错误
  • 需要多日通票功能应使用 listingType: TICKET

5. 手续费验证

  • 前端传递的 fees 必须与后端计算一致
  • 验证函数: checkTicketVariantPrices()
  • 容差范围: ±0.01(考虑浮点数精度)

注意事项

1. 权限要求

  • 用户必须是 BUSINESS_PARTNER 角色
  • 未登录用户返回 401 Unauthorized
  • 权限不足返回 403 Forbidden

2. Event Catalog 自动创建

  • 首次创建 ANY_DAY_TICKET 时自动创建 Event Catalog
  • Event Catalog 的 catalogType = 'EVENT'
  • 无需手动创建商家/目录

3. 交付方式限制

交付方式 说明
QR_CODE ✅ 推荐 - 二维码核销
IN_PERSON ✅ 支持 - 现场交付
THIRD_PARTY_ISSUED ✅ 支持 - 第三方发行

不支持:

  • SHIPPING - ANY_DAY_TICKET 不支持配送

4. 图片要求

  • 至少 1 张图片(images 数组非空)
  • 建议包含封面图片(setThumbnail: true
  • 使用 Cloudinary 的 public_id 作为图片 ID

5. 变体要求

  • 至少 1 个变体
  • price 必须提供(字符串或数字格式)
  • inventoryQuantity 必须为非负整数
  • option 如不提供会自动生成(基于 options 配置)

6. 税率配置

参数 说明 默认值
taxEnable 是否启用税收 false
overrideEventTax 是否覆盖 Event 税率 false
customTaxRate 自定义税率 (0-100) -

7. 门票筛选配置 (atDoorTicketConfig)

字段 类型 说明
includeTitleText `string \ null` 仅显示包含该标题的活动
excludeTitleText `string \ null` 排除包含该标题的活动
eventSelectionWindowHours `number \ null` 时间窗口(小时,≥ 1)

相关接口

接口 方法 说明
/v2/products GET 通用产品列表
/v2/products/any-day-tickets GET ANY_DAY_TICKET 产品列表
/v2/products/:id GET 获取单个产品详情
/v2/products/:id PUT 更新产品
/v2/products/:id DELETE 删除产品(软删除)
/v2/products/batch POST 批量导入产品
/product-event/v2/upcoming GET 获取可选活动列表

相关文档

  • [[POST /v2/products/any-day-tickets - 列出门票产品]] - ANY_DAY_TICKET 列表接口
  • [[Product V2 Module]] - 产品 V2 模块文档
  • [[KAT-10350-ANY-DAY-TICKET-DESIGN]] - ANY_DAY_TICKET 技术设计文档
  • [[At-Door Ticket Config (KAT-10412)]] - 门票筛选配置说明
  • [[Product Form (KAT-8634)]] - 产品表单功能

变更历史

版本 日期 变更内容
v0.0.28 2026-02-XX 初始版本(KAT-10350 Phase 6)
v0.0.29 2026-03-02 添加产品表单支持 (KAT-8634)
v0.0.30 2026-03-02 添加门票筛选配置 (KAT-10412)
v0.0.31 2026-03-02 添加数据库设计和 SQL 部分

results matching ""

    No results matching ""